Add UniDiffuser model and pipeline#2963
Conversation
|
The documentation is not available anymore as the PR was closed or merged. |
|
Currently, the code in the PR isn't in a working state, and I haven't implemented tests or tested the code yet. I've opened the PR because I wanted to get some preliminary feedback on the design and code. In particular, I have the following questions: Design Questions:
Would it be better if I split
Questions about Tests:
[if there is a better place to move this discussion, please let me know :) ] |
| def __init__( | ||
| self, | ||
| tokenizer: GPT2Tokenizer, | ||
| text_decoder: GPT2LMHeadModel, |
There was a problem hiding this comment.
Can we try to seperate the tokenizer and text decoder here.
diffusers should be able to load the tokenizer out of the box, you just have to define it in the pipeline, e.g. here: https://github.com/huggingface/diffusers/pull/2963/files#r1159526676
There was a problem hiding this comment.
Also we cannot pass the text_decoder here at init as this would prevent us to be able to use from_pretained(...) of the model class. Could you maybe try to follow the design as done here:
See how we import blocks from transformers to design our own new model. I think here you could just do the following:
def __init__(
self,
num_layers=12,
...
):
config = GPT2Config(...take all the config params from init)
self.text_decoder = GPT2LMHeadModel(config)We then design a new checkpoint architecture for the UniDiffusersTextDecoder and upload pretrained weights for it
There was a problem hiding this comment.
Changed the design of __init__ following the example: removed tokenizer and text_decoder args, added GPT2 config args.
| eos = "<|EOS|>" | ||
| special_tokens_dict = {"eos_token": eos} | ||
| self.tokenizer = tokenizer | ||
| self.tokenizer.add_special_tokens(special_tokens_dict) |
There was a problem hiding this comment.
Note that we can do this directly for the uploaded tokenizer. E.g. let's just upload a tokenizer that has EOS already added so that we don't have to do it every time we call the model at init
More than happy to help here later on!
There was a problem hiding this comment.
Removed the tokenizer logic from __init__, will work on uploading the appropriate tokenizer.
There was a problem hiding this comment.
I've prepared some native diffusers checkpoints for the current implementation of the UniDiffuserPipeline and its building blocks (e.g. UniDiffuserModel, UniDiffuserTextDecoder, etc.) [see the convert_to_ckpt.py script]. How can I upload these up to the hub?
There was a problem hiding this comment.
I was able to upload some models to the hub (see e.g. small test models here), but I'm confused about how to save/push to hub a tokenizer with added special tokens. The documentation for PreTrainedTokenizerBase.from_pretrained says that it won't save modifications to the tokenizer after initialization and I wasn't able to find any resources on how to do it after searching.
For reference, the code in the base unidiffuser library is something like
eos = '<|EOS|>'
special_tokens_dict = {'eos_token': eos}
base_tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
base_tokenizer.add_special_tokens(special_tokens_dict)There was a problem hiding this comment.
Regarding uploading the weights and the new tokenizer, you can can call push_to_hub() directly on the model.
So, for example (considering UniDiffuserModel is already populated with the pre-trained checkpoints):
unidiffusers = UniDiffuserModel(...)
unidiffusers.push_to_hub("your_hub_user_name/model_id")Same applies for the rest of the models and the tokenizer.
| self.transformer = text_decoder | ||
| # TODO: need to set the eos_token_id correctly | ||
| self.transformer.config.eos_token_id = self.tokenizer.eos_token_id | ||
| self.transformer.resize_token_embeddings(len(self.tokenizer)) |
There was a problem hiding this comment.
We can also make sure that the GPT2Transformer has the correct number of word embeddings before loading it so that we don't have to always resize the embedding every time at init
There was a problem hiding this comment.
+1. I would prefer to have the decoder with the rejigged embeddings on the Hub rather than rejigging on the fly.
| return generated_captions | ||
|
|
||
| @torch.no_grad() | ||
| def generate_beam( |
| """ | ||
|
|
||
| @register_to_config | ||
| def __init__( |
There was a problem hiding this comment.
The design here looks good to me! Note that I think we can remove some redundant code that is not needed for this use case. I think you only need one of the three cases:
self.is_input_continuous = (in_channels is not None) and (patch_size is None)
self.is_input_vectorized = num_vector_embeds is not None
self.is_input_patches = in_channels is not None and patch_size is not NoneThere was a problem hiding this comment.
Should have removed most of the redundant code (kept only the code handling the patch input case, since that's what the original UniDiffuser implementation used).
|
Great first design! I left some comments directly in the code. In short I think the general design is very nice - the models should be defined under the pipeline folder just like you do and the pipeline also looks quite nice already. Answering your questions in line
I think since the purpose of UniDiffusers is exactly to bring all modes into the same distribution, one pipeline is nice here. So this design works for me. I'd maybe just not have a "mode" call input, but instead automatically decide the mode depending on what the user puts in. E.g. if the user just passes a "text" input, we're in text2img mode, if just a "image" input, we're in image to text mode => would this design work or are the inputs not enough to define which mode one is in? E.g. are muiltple modes possible for the same input combination?
Left comments mostly directly in the code. In short:
Not really. Some guides that could help:
Regarding tiny models, yeah we just create them ourselves. What you can do here is to just load tiny configs to create random tiny models and use those for faster testing :-)
Hope this helps a bit so that you can move forward, let me know if you need more help! |
|
Thanks for the review! With regards to this:
for the currently supported modes, there is some ambiguity when neither text nor image input is provided. In this case, we cannot be sure whether the user wants unconditional ("marginal") image generation, unconditional ("marginal") text generation, or joint image-text generation. The original code additionally supports image variation ("img2text2img") and text variation ("text2img2text") modes, whose inputs would be the same as the image-to-text (a conditioning image) and text-to-image (a conditioning prompt) modes, respectively. So supporting these modes would also cause some ambiguity. So perhaps we could infer the [Just as a side note, the image variation implementation is different between [Edit: pushed new commit with possible implementation as described above] |
|
I have referenced some codes of yours and combined with mine, and also submited an initial version PR PaddlePaddle/PaddleNLP#5487 , hope to learn from each other and contribute to the community |
|
Hi @patrickvonplaten and @baofff, In looking at the noise prediction model architecture, I'm using
In light of this, I have the following questions:
|
|
As a note, if you want to look at the code I used to calculate the |
|
Very cool! This looks like almost ready to be merged to me - thanks a lot for re-iterating on the design :-) |
|
@williamberman @sayakpaul when you have a moment, it'd be super cool if you could review |
| stop_token: str = "<|EOS|>", | ||
| ): | ||
| """ | ||
| Generates text using the given tokenizer and text prompt or token embedding via beam search. |
There was a problem hiding this comment.
Help me understand this a bit. Why would there be a need to generate text from a given text prompt?
There was a problem hiding this comment.
Sorry, I think I wrote this docstring in a confusing way. In the context of UniDiffuser sampling, we use this function to generate output text (when appropriate) from the text latents after we process the CLIP-embedded input prompt using the unet (UniDiffuserModel) model. The method accepts both prompt and embed arguments, for input tokens and embeddings respectively, but we only ever call it with input embeddings (as described above):
There was a problem hiding this comment.
Oh okay. Yeah, then I guess we need to it make it a bit clearer from the code?
| labels (`torch.Tensor`, *optional*): | ||
| TODO | ||
| """ | ||
| embedding_text = self.transformer.transformer.wte(tokens) |
There was a problem hiding this comment.
Why transformer.transformer?
There was a problem hiding this comment.
I took the forward(...) method from the original code. Upon reviewing the code, I think the forward method was probably intended to do the following: the tokens argument is a sequence of input vocab token IDs for the GPT2LMHeadModel, while the prefix argument is the hidden state of another model (e.g. something like transformers.modeling_outputs.BaseModelOutputWithPooling.last_hidden_state of a CLIPTextModel). prefix then gets converted to an intermediate representation via self.encode_prefix(...) and then converted into the latent space of the GPT model via self.decode_prefix(...) (if they are being used). We then combine the embedding of tokens with the prefix embedding and then do a forward pass of the internal GPT2LMHeadModel.
I guess it's confusing currently because on lines 52-54 instead of using n_embd as the input dimension to nn.Linear we should instead have a new argument prefix_inner_dim and use that, e.g.
self.encode_prefix = (
nn.Linear(prefix_inner_dim, self.prefix_hidden_dim) if self.prefix_hidden_dim is not None else nn.Identity()
)Furthermore, prefix_hidden_dim should probably always need to be supplied, since prefix_inner_dim and n_embd are in general not guaranteed to be the same.
There was a problem hiding this comment.
Thanks a lot for explaining!
I guess we will know better once we start testing the code.
|
Design looks quite clean and matured to me. My questions / comments are very minor ones and are probably already covered by @patrickvonplaten's comments.
+1 to this. |
|
I've uploaded a I've also opened a PR at |
|
This is great! Thanks so much for your efforts. I think now the TODOs are:
I have also merged your PR. So, hopefully, this unblocks you. @patrickvonplaten can help us with the repo transfers. |
|
Let me know once you need help with a model transfer |
* Fix a bug of pano when not doing CFG * enhance code quality * apply formatting. --------- Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
* fix progress bar issue in pipeline_text_to_video_zero.py. Copy scheduler after first backward * fix tensor loading in test_text_to_video_zero.py * make style && make quality
* fix: norm group test for UNet3D. * chore: speed up the panorama tests (fast). * set default value of _test_inference_batch_single_identical. * fix: batch_sizes default value.
| | Pipeline | Tasks | Demo | ||
| |---|---|:---:| | ||
| | [UniDiffuserPipeline](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/pipeline_unidiffuser.py) | *Joint Image-Text Gen*, *Text-to-Image*, *Image-to-Text*, *Image Gen*, *Text Gen*, *Image Variation*, *Text Variation* | | |
There was a problem hiding this comment.
| | Pipeline | Tasks | Demo | |
| |---|---|:---:| | |
| | [UniDiffuserPipeline](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/pipeline_unidiffuser.py) | *Joint Image-Text Gen*, *Text-to-Image*, *Image-to-Text*, *Image Gen*, *Text Gen*, *Image Variation*, *Text Variation* | | | |
| | Pipeline | Tasks | Demo | Colab | | |
| |---|---|:---:| | |
| | [UniDiffuserPipeline](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/pipeline_unidiffuser.py) | *Joint Image-Text Gen*, *Text-to-Image*, *Image-to-Text*, *Image Gen*, *Text Gen*, *Image Variation*, *Text Variation* | [🤗 Spaces](https://huggingface.co/spaces/thu-ml/unidiffuser) | [](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/unidiffuser.ipynb) | |
For now, let's add a link to the original demo. @hysts is working on to change the demo to have diffusers usage.
There was a problem hiding this comment.
Prepared a Colab Notebook from your awesome documentation: huggingface/notebooks#377
Also prepared this GIF to showcase the powerfulness of the pipeline:

| import requests | ||
| import torch | ||
| from PIL import Image | ||
| from io import BytesIO | ||
|
|
||
| from diffusers import UniDiffuserPipeline | ||
|
|
||
| device = "cuda" | ||
| model_id_or_path = "thu-ml/unidiffuser-v1" | ||
| pipe = UniDiffuserPipeline.from_pretrained(model_id_or_path, torch_dtype=torch.float16) | ||
| pipe.to(device) | ||
|
|
||
| # Image-to-text generation | ||
| image_url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/unidiffuser/unidiffuser_example_image.jpg" | ||
| response = requests.get(image_url) | ||
| init_image = Image.open(BytesIO(response.content)).convert("RGB") | ||
| init_image = init_image.resize((512, 512)) | ||
|
|
||
| sample = pipe(image=init_image, num_inference_steps=20, guidance_scale=8.0) | ||
| i2t_text = sample.text[0] | ||
| print(text) |
There was a problem hiding this comment.
| import requests | |
| import torch | |
| from PIL import Image | |
| from io import BytesIO | |
| from diffusers import UniDiffuserPipeline | |
| device = "cuda" | |
| model_id_or_path = "thu-ml/unidiffuser-v1" | |
| pipe = UniDiffuserPipeline.from_pretrained(model_id_or_path, torch_dtype=torch.float16) | |
| pipe.to(device) | |
| # Image-to-text generation | |
| image_url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/unidiffuser/unidiffuser_example_image.jpg" | |
| response = requests.get(image_url) | |
| init_image = Image.open(BytesIO(response.content)).convert("RGB") | |
| init_image = init_image.resize((512, 512)) | |
| sample = pipe(image=init_image, num_inference_steps=20, guidance_scale=8.0) | |
| i2t_text = sample.text[0] | |
| print(text) | |
| from diffusers import UniDiffuserPipeline | |
| from diffusers.utils import load_image | |
| device = "cuda" | |
| model_id_or_path = "thu-ml/unidiffuser-v1" | |
| pipe = UniDiffuserPipeline.from_pretrained(model_id_or_path, torch_dtype=torch.float16) | |
| pipe.to(device) | |
| # Image-to-text generation | |
| image_url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/unidiffuser/unidiffuser_example_image.jpg" | |
| init_image = load_image(image_url).resize((512, 512)) | |
| sample = pipe(image=init_image, num_inference_steps=20, guidance_scale=8.0) | |
| i2t_text = sample.text[0] | |
| print(i2t_text) |
Reduces the LoC :)
| # Image variation can be performed with a image-to-text generation followed by a text-to-image generation: | ||
| # 1. Image-to-text generation | ||
| image_url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/unidiffuser/unidiffuser_example_image.jpg" | ||
| response = requests.get(image_url) | ||
| init_image = Image.open(BytesIO(response.content)).convert("RGB") | ||
| init_image = init_image.resize((512, 512)) |
There was a problem hiding this comment.
I guess we can follow the same as https://github.com/huggingface/diffusers/pull/2963/files#r1205061596 for loading and resizing the image?
| - all | ||
| - __call__ | ||
|
|
||
| ## ImageTextPipelineOutput |
There was a problem hiding this comment.
There was a problem hiding this comment.
Also wanted to know if there's any argument control the number of images / text I wanted to generate as a part of the variation mode.
There was a problem hiding this comment.
I can control the num_images_per_prompt in the text-to-image mode, so that's settled. But what about text variation?
There was a problem hiding this comment.
For modes which generate only text (img2text and text), there's an analogous num_prompts_per_image argument to __call__ . So when you perform the second img2text generation for text variation you can specify num_prompts_per_image > 1 to get multiple text variation samples.
There was a problem hiding this comment.
This should go here:
https://github.com/huggingface/diffusers/blob/main/docs/source/en/api/outputs.mdx
It feels more natural to me to have the documentation for ImageTextPipelineOutput alongside ImagePipelineOutput, which is at the Diffusion Pipeline doc page.
There was a problem hiding this comment.
I've gone ahead and moved the ImageTextPipelineOutput documentation to /api/diffusion_pipeline.mdx (alongside the ImagePipelineOutput and AudioPipelineOutput documentation). Let me know if it would be better somewhere else (for example, at /api/outputs.mdx as originally suggested) :).
There was a problem hiding this comment.
I understand. But we will soon update that too :)
There was a problem hiding this comment.
I see, so would it be better if I move it to /api/outputs? Or is it fine to leave it at /api/diffusion_pipeline for now?
There was a problem hiding this comment.
Let's keep it as is for now. Then we will bulk move things :)
|
|
||
| sample = pipe(image=init_image, num_inference_steps=20, guidance_scale=8.0) | ||
| i2t_text = sample.text[0] | ||
| print(text) |
|
|
||
| ### Unconditional Image and Text Generation | ||
|
|
||
| Unconditional generation (where we start from only latents sampled from a standard Gaussian prior) from a `UniDiffuserPipeline` will produce a (image, text) pair: |
There was a problem hiding this comment.
| Unconditional generation (where we start from only latents sampled from a standard Gaussian prior) from a `UniDiffuserPipeline` will produce a (image, text) pair: | |
| Unconditional generation (where we start from only latents sampled from a standard Gaussian prior) from a [`UniDiffuserPipeline`] will produce a (image, text) pair: |
So, that the hyperlink is automatically rendered.
| print(text) | ||
| ``` | ||
|
|
||
| The `img2text` mode requires that an input `image` be supplied. You can set the `img2text` mode manually with [`UniDiffuser.set_image_to_text_mode`]. |
There was a problem hiding this comment.
| The `img2text` mode requires that an input `image` be supplied. You can set the `img2text` mode manually with [`UniDiffuser.set_image_to_text_mode`]. | |
| The `img2text` mode requires that an input `image` be supplied. You can set the `img2text` mode manually with [`UniDiffuserPipeline.set_image_to_text_mode`]. |
…fuser.mdx to /api/diffusion_pipeline.mdx.
| self.transformer = GPT2LMHeadModel(gpt_config) | ||
|
|
||
| def forward( | ||
| self, |
| return self.encode_prefix(prefix) | ||
|
|
||
| @torch.no_grad() | ||
| def generate_captions(self, features, eos_token_id, device): |
| eos_token_id: Optional[int] = None, | ||
| input_ids=None, | ||
| input_embeds=None, | ||
| device=None, | ||
| beam_size: int = 5, | ||
| entry_length: int = 67, | ||
| temperature: float = 1.0, |
There was a problem hiding this comment.
| eos_token_id: Optional[int] = None, | |
| input_ids=None, | |
| input_embeds=None, | |
| device=None, | |
| beam_size: int = 5, | |
| entry_length: int = 67, | |
| temperature: float = 1.0, | |
| input_ids=None, | |
| input_embeds=None, | |
| device=None, | |
| beam_size: int = 5, | |
| entry_length: int = 67, | |
| temperature: float = 1.0, | |
| eos_token_id: Optional[int] = None, |
(nit) Let's change the order here maybe since the eos_token_id should probably not be the first input
| cross_attention_kwargs=None, | ||
| class_labels=None, | ||
| ): | ||
| # Pre-LayerNorm |
patrickvonplaten
left a comment
There was a problem hiding this comment.
Great this PR looks good to go for me! Just one final tiny nit regarding the ordering of the generate input.
Apart from this, this is good to merge from my side :-) Incredible work here @dg845! This is really a difficult model with many components and the final implementation is super nice :-)
|
@dg845 once the conflicts are resolved and tests pass, we will merge :) Meanwhile, I will also correct the gif. Really amazing contribution. I hope the contribution experience was enjoyable for you. |
|
@patrickvonplaten a friendly ping for these transfers: |
|
Thanks! I really enjoyed working on this PR :). And thanks for all the advice and help along the way :). |
|
@sayakpaul feel free to merge whenever! All good from my side |
|
@dg845 thanks again for your amazing contribution. The pipeline and the components are now live at: https://huggingface.co/docs/diffusers/main/en/api/pipelines/unidiffuser |
This PR implements a pipeline for the UniDiffuser model as discussed in #2857.
Model/Pipeline Description
The UniDiffuser model (paper, code) is a multi-modal model which extends the DDPM model to model all distributions relevant to a set of multi-modal data. From the paper abstract:
In this PR, we implement a image-text UniDiffuser model as described in the paper:
Usage Examples
TODO
Discussion
CC
@patrickvonplaten
@nemonameless
@baofff (author on original paper, author of original code)